package tech.xs.gateway.filter;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import tech.xs.auth.api.AuthPathApi;
import tech.xs.common.auth.domain.cache.AuthPathCache;
import tech.xs.common.auth.domain.cache.AuthTokenCache;
import tech.xs.common.auth.domain.constant.AuthApiConstant;
import tech.xs.common.auth.util.AuthTokenUtil;
import tech.xs.common.framework.domain.constant.ResultConstant;
import tech.xs.common.framework.domain.constant.Symbol;
import tech.xs.common.framework.domain.enums.CommonResultEnum;
import tech.xs.common.framework.domain.model.ApiResult;
import tech.xs.common.framework.util.AuthHeaderUtil;
import tech.xs.common.framework.util.PermissionUtil;
import tech.xs.common.framework.util.RoleUtil;

import javax.annotation.Resource;
import java.util.Set;

@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    public static final String REQUEST_ID = "request_id";

    @Resource
    private AuthPathApi authPathApi;

    @Resource
    private RedissonClient redissonClient;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders headers = request.getHeaders();
        request = exchange.getRequest().mutate().header(REQUEST_ID, IdUtil.getSnowflakeNextIdStr()).build();
        String path = request.getPath().value();
        String method = request.getMethod().name();
        try {
            log.info("网络请求: " + path + " " + method);
            String cacheKey = AuthApiConstant.AUTH_PATH_CACHE_KEY + method.toUpperCase() + Symbol.COLON + path;
            RBucket<AuthPathCache> bucket = redissonClient.getBucket(cacheKey);
            AuthPathCache authPath = bucket.get();
            if (authPath == null) {
                log.info("授权失败,可能存在以下原因\n 1.请求url错误\n2.url未配置授权\n3.url所属服务未启动或启动失败\n4.授权服务未启动\n method: {} path:{}", method, path);
                return error(response, CommonResultEnum.permissionDenied);
            }
            if (authPath.isAllWhite()) {
                return success(exchange, chain, request);
            }

            AuthHeaderUtil.AuthHeader headToken = AuthHeaderUtil.getAuthHeader(request);
            if (headToken == null) {
                log.info("账户未登录 无接口访问权限 method: {} path:{}", method, path);
                return error(response, CommonResultEnum.noLogin);
            }

            switch (headToken.getMode()) {
                case Token: {
                    return isPathAuthToken(exchange, chain, headToken, authPath, path, method);
                }
                case Basic: {
                    return isPathAuthBasic(exchange, chain, headToken, path, method, headers);
                }
                default: {
                    log.info("鉴权方式错误 method: {} path:{}", method, path);
                    return error(response, CommonResultEnum.permissionDenied);
                }
            }
        } catch (Exception e) {
            log.error("授权失败", e);
            return error(response, CommonResultEnum.unknownError);
        }
    }

    private Mono<Void> isPathAuthToken(ServerWebExchange exchange, GatewayFilterChain chain, AuthHeaderUtil.AuthHeader headToken, AuthPathCache authPath, String path, String method) {
        ServerHttpResponse response = exchange.getResponse();
        String accessToken = headToken.getAccessToken();
        AuthTokenCache token = AuthTokenUtil.getToken(redissonClient, headToken.getUserId(), headToken.getClientType());
        if (token == null || !accessToken.equals(token.getAccessToken())) {
            log.info("登陆失效 无接口访问权限 method: {} path:{} userName:{} clientType:{} accessToken:{}", method, path, headToken.getUserId(), headToken.getClientType(), accessToken);
            return error(response, CommonResultEnum.loginInvalid);
        }
        AuthTokenUtil.refreshAuthToken(redissonClient, token);
        Set<String> userRoles = token.getRoleCodes();
        return pathAuthCheck(exchange, chain, authPath, userRoles);
    }

    private Mono<Void> pathAuthCheck(ServerWebExchange exchange, GatewayFilterChain chain, AuthPathCache authPath, Set<String> userRoles) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        if (authPath.isLoginWhite()) {
            return success(exchange, chain, request);
        }
        Set<String> pathPermissions = authPath.getPermissions();
        Set<String> pathRoles = authPath.getRoles();
        userRoles = RoleUtil.getEnableRole(userRoles);
        if (userRoles != null && pathRoles != null) {
            for (String userRole : userRoles) {
                if (pathRoles.contains(userRole)) {
                    return success(exchange, chain, request);
                }
            }
        }
        Set<String> userPermissions = PermissionUtil.getEnableRolePermission(userRoles);
        if (userPermissions != null && pathPermissions != null) {
            for (String userPermission : userPermissions) {
                if (pathPermissions.contains(userPermission)) {
                    return success(exchange, chain, request);
                }
            }
        }
        return error(response, CommonResultEnum.permissionDenied);
    }

    private Mono<Void> isPathAuthBasic(ServerWebExchange exchange, GatewayFilterChain chain, AuthHeaderUtil.AuthHeader headToken, String path, String method, MultiValueMap<String, String> headers) {
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();

        String userName = headToken.getUserName();
        String password = headToken.getPassword();
        if (StrUtil.isBlank(userName) || StrUtil.isBlank(password)) {
            return error(response, ApiResult.error("用户名和密码都不能为空"));
        }
        ApiResult<Boolean> pathAuth = authPathApi.isPathAuthBasic(path, method, headers);
        if (ResultConstant.SUCCESS_CODE == pathAuth.getCode()) {
            if (pathAuth.getData() != null && pathAuth.getData()) {
                return success(exchange, chain, request);
            }
        }
        log.info("Basic授权失败,无权限 " + path + " " + method);
        return error(response, CommonResultEnum.permissionDenied);
    }

    private Mono<Void> success(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
        return chain.filter(exchange.mutate().request(request).build());
    }

    private Mono<Void> error(ServerHttpResponse response, ApiResult<Boolean> pathAuth) {
        if (pathAuth != null) {
            HttpStatus status = HttpStatus.resolve(pathAuth.getCode());
            if (status != null) {
                response.setStatusCode(status);
            } else {
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
            }
        } else {
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        DataBuffer buffer = response.bufferFactory().wrap(JSONObject.toJSONBytes(pathAuth));
        return response.writeWith(Mono.just(buffer));
    }

    private Mono<Void> error(ServerHttpResponse response, CommonResultEnum resultEnum) {
        return error(response, ApiResult.error(resultEnum));
    }

    @Override
    public int getOrder() {
        return -100;
    }

}
